package icasue.base.utils.bind;

import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValues;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertyResolver;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2020/7/10 下午8:29
 * @UpdateDate:
 * @Description:
 */
public class SpringBootBindUtil {

    public static final MethodHandles.Lookup lookup;
    public static final IBind BIND;

    static {
        //get lookup
        MethodHandles.Lookup lookupTemp = null;
        try {
            //java 9
            Method lookupMethod = MethodHandles.class.getDeclaredMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class);
            lookupMethod.setAccessible(true);
            MethodHandles.Lookup lookup =
                    (MethodHandles.Lookup)lookupMethod.invoke(null, MethodHandles.Lookup.class, MethodHandles.lookup());
            lookupTemp = lookup;
        }catch (Exception e){
            //java 8
            try {
                Constructor<MethodHandles.Lookup> lookupConstruct = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
                lookupConstruct.setAccessible(true);
                lookupTemp = lookupConstruct.newInstance(MethodHandles.Lookup.class,  MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
                        | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC);
            } catch (Exception e1) {
                throw new RuntimeException(e1.getCause());
            }
        }
        lookup = lookupTemp;


        //--- Init Ibind
        IBind bind;
        try {
            //boot2
            Class.forName("org.springframework.boot.context.properties.bind.Binder");
            bind = new SpringBoot2xBind();
        } catch (Exception e) {
            //boot1
            bind = new SpringBoot1xBind();
        }
        BIND = bind;
    }

    public static <T> T bind(Environment environment,String prefix, Class<T> targetClass){
        T bind = BIND.bind(environment, prefix, targetClass);
        return bind;
    }

    @FunctionalInterface
    public interface IBind{
        public <T> T bind(Environment environment, String prefix, Class<T> targetClass);
    }

    public static class SpringBoot2xBind implements IBind {
        @Override
        public <T> T bind(Environment environment, String prefix, Class<T> targetClass) {
            T target = null;
            try {
                Class<?> bindClass = Class.forName("org.springframework.boot.context.properties.bind.Binder");
                Class<?> environmentClass = Class.forName("org.springframework.core.env.Environment");
                Class<?> bindResultClass = Class.forName("org.springframework.boot.context.properties.bind.BindResult");
                MethodHandle binderGetHandle = lookup.findStatic(bindClass, "get", MethodType.methodType(bindClass, environmentClass));
                MethodHandle bindHandle = lookup.findVirtual(bindClass, "bind", MethodType.methodType(bindResultClass,String.class, Class.class));
                Object binder = binderGetHandle.invoke(environment);
                Object bindResult = bindHandle.bindTo(binder).invoke(prefix, targetClass);
                MethodHandle isBoundHandle = lookup.findVirtual(bindResult.getClass(), "isBound", MethodType.methodType(boolean.class));
                boolean isBound = (boolean)isBoundHandle.bindTo(bindResult).invoke();
                if(isBound){
                    Method getMethod = bindResult.getClass().getDeclaredMethod("get");
                    target = (T)getMethod.invoke(bindResult);
                }
            }catch (Throwable e){
                target = null;
            }finally {
                if(target == null){
                    try {
                        target = targetClass.newInstance();
                    }catch (Throwable e2){
                        target = null;
                    }
                }
            }
            return target;
        }
    }

    public static class SpringBoot1xBind implements IBind{

        @Override
        public <T> T bind(Environment environment, String prefix, Class<T> targetClass) {
            T target = null;
            try {
                Class<?> resolverClass = Class.forName("org.springframework.boot.bind.RelaxedPropertyResolver");
                Constructor<?> resolverConstructor = resolverClass.getDeclaredConstructor(PropertyResolver.class);
                Method getSubPropertiesMethod = resolverClass.getDeclaredMethod("getSubProperties", String.class);
                Object resolver = resolverConstructor.newInstance(environment);
                Map<String, Object> properties = (Map<String, Object>) getSubPropertiesMethod.invoke(resolver, "");
                target = targetClass.newInstance();
                Class<?> binderClass = Class.forName("org.springframework.boot.bind.RelaxedDataBinder");
                Constructor<?> binderConstructor = binderClass.getDeclaredConstructor(Object.class, String.class);
                Method bindMethod = binderClass.getMethod("bind", PropertyValues.class);
                Object binder = binderConstructor.newInstance(target, prefix);
                bindMethod.invoke(binder, new MutablePropertyValues(properties));
                return target;
            } catch (Exception e) {
                try {
                    target = targetClass.newInstance();
                }catch (Throwable e2){
                    target = null;
                }
            }
            return target;
        }
    }

}
